﻿using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Nancy;
using Nancy.Responses.Negotiation;
using QQ2564874169.Core;

namespace QQ2564874169.WebFx.Nancy.Web
{
    public abstract class FxController : NancyModule
    {
        public static event Action<FxController> Created;
        private static SettingContainer _routeSettings = new SettingContainer();

        public event EventHandler<ActionEventArgs> ActionBefore;
        public event EventHandler<ActionEventArgs> ActionAfter;
        public event EventHandler<ActionErrorEventArgs> ActionError;
        public TempData TempData { get; private set; }
        public RouteSetting CurrentRoute => Context.Items[_currroutekey] as RouteSetting;

        private static string _currroutekey = Guid.NewGuid().ToString();

        protected FxController()
        {
            TempData = new TempData(this);
            BindAction();
            Created?.Invoke(this);
        }

        protected virtual object OnActionBefore(ActionEventArgs e)
        {
            ActionBefore?.Invoke(this, e);
            return e.Response;
        }

        protected virtual void OnActionAfter(ActionEventArgs e)
        {
            ActionAfter?.Invoke(this, e);
        }

        protected virtual void OnActionError(ActionEventArgs args, Exception ex)
        {
            var e = new ActionErrorEventArgs
            {
                Ex = ex,
                Context = args.Context,
                Parameters = args.Parameters,
                Route = args.Route,
                Response = args.Response
            };
            ActionError?.Invoke(this, e);
            args.Response = e.Response;
        }

        protected virtual string OnPath(string path)
        {
            var items = path.TrimStart('/').Split('/').ToArray();
            if (items.Length > 1)
            {
                const string repstr = "controller";
                var ctrl = items[0];
                if (ctrl.EndsWith(repstr, StringComparison.OrdinalIgnoreCase))
                    items[0] = ctrl.Substring(0, ctrl.Length - repstr.Length);
            }
            return string.Join("/", items);
        }

        protected virtual RouteSetting OnRouteSetting(RouteSetting route)
        {
            if (route.Action.DeclaringType == null)
                throw new ArgumentNullException();

            var dir = route.Action.DeclaringType.Name;
            var attr = route.Action.DeclaringType.GetCustomAttribute<ControllerNameAttribute>();
            if (attr != null)
            {
                dir = attr.Name;
            }
            if (route.Routes.Count < 1)
            {
                var path = ("/" + dir + "/" + route.Action.Name).QQSplit('/').QQJoin("/");
                route.Routes.Add("/" + path);
            }
            for (var i = 0; i < route.Routes.Count; i++)
            {
                var path = route.Routes[i].Trim();

                if (path != "/" && path.EndsWith("/"))
                    path = path.TrimEnd('/');

                if (path.StartsWith("/") == false)
                {
                    path = "/" + dir + "/" + path;
                }
                route.Routes[i] = OnPath(path);
            }
            
            return route;
        }

        protected virtual RouteSetting[] LoadAction(Type controllerType)
        {
            if (controllerType == null) 
                throw new ArgumentNullException(nameof(controllerType));
            var list = new List<RouteSetting>();
            var methods = controllerType.GetMethods(BindingFlags.Instance | BindingFlags.Public |
                                                    BindingFlags.NonPublic | BindingFlags.DeclaredOnly);
            foreach (var m in methods)
            {
                foreach (var attr in m.GetCustomAttributes<RouteSetting>().ToArray())
                {
                    var item = attr;
                    item.Action = m;
                    item = OnRouteSetting(item);
                    if (item.Routes != null && item.Routes.Count > 0 && item.Action != null)
                    {
                        list.Add(item);
                    }
                }
            }

            return list.ToArray();
        }

        private Func<dynamic, dynamic> CreateFunc(RouteSetting route)
        {
            return p =>
            {
                Context.Items.Add(_currroutekey, route);
                var ps = new DynamicDictionary();
                if(Request.Query is DynamicDictionary q)
                {
                    foreach (var k in q.Keys)
                    {
                        ps[k] = q[k];
                    }
                }

                if (Request.Form is DynamicDictionary f)
                {
                    foreach (var k in f.Keys)
                    {
                        ps[k] = f[k];
                    }
                }

                if (Context.Parameters is DynamicDictionary cp)
                {
                    foreach (var k in cp.Keys)
                    {
                        ps[k] = cp[k];
                    }
                }

                var args = new ActionEventArgs
                {
                    Context = Context,
                    Parameters = ps,
                    Route = route
                };
                try
                {
                    OnActionBefore(args);

                    if (args.Response != null)
                    {
                        return args.Response;
                    }

                    var mps = route.Action.GetParameters();
                    if (mps.Length == 0)
                    {
                        args.Response = route.Action.Invoke(this, new object[0]);
                    }
                    else if (mps.Length == 1 && mps[0].ParameterType == typeof(object))
                    {
                        args.Response = route.Action.Invoke(this, new [] { args.Parameters });
                    }
                    else
                    {
                        throw new MethodAccessException("Action只接受0参数或者一个dynamic参数的方法");
                    }
                }
                catch (Exception ex)
                {
                    var err = ex;
                    if (ex is TargetInvocationException && ex.InnerException != null)
                    {
                        err = ex.InnerException;
                    }

                    args.Response = null;
                    OnActionError(args, err);
                    if (args.Response == null)
                        throw err;
                }
                finally
                {
                    OnActionAfter(args);
                    Context.Items.Remove(_currroutekey);
                }

                return args.Response;
            };
        }

        private void BindAction()
        {
            var selfType = GetType();

            if (selfType.FullName == null) return;

            if (_routeSettings.ContainsKey(selfType.FullName) == false)
            {
                var actions = LoadAction(selfType);

                _routeSettings.TryAdd(selfType.FullName, actions);
            }

            foreach (var item in _routeSettings[selfType.FullName].ToArray())
            {
                var action = CreateFunc(item);
                foreach (var path in item.Routes)
                {
                    if (item.Method.HasFlag(RequestMethod.Delete))
                    {
                        Delete(path, action);
                    }
                    if (item.Method.HasFlag(RequestMethod.Put))
                    {
                        Put(path, action);
                    }
                    if (item.Method.HasFlag(RequestMethod.Post))
                    {
                        Post(path, action);
                    }
                    if (item.Method.HasFlag(RequestMethod.Get))
                    {
                        Get(path, action);
                    }
                }
            }
        }

        protected virtual Negotiator GetView(string path, object model)
        {
            var final = path;
            if (string.IsNullOrEmpty(path))
            {
                final = CurrentRoute.Action.Name;
            }

            if (final.StartsWith("/") == false)
            {
                final = string.Join("/", GetType().Name, final);
            }

            if (string.IsNullOrEmpty(path))
            {
                final = OnPath(final);
            }

            var v = View[final, model];
            v.WithContentType("text/html;charset=utf-8");
            return v;
        }

        protected Negotiator GetViewByPath(string path)
        {
            return GetView(path, null);
        }

        protected Negotiator GetViewByModel(object model)
        {
            return GetView(null, model);
        }

        protected Negotiator GetView()
        {
            return GetView(null, null);
        }

        private class SettingContainer : ConcurrentDictionary<string, RouteSetting[]>
        {

        }
    }
}
